package com.hj.excel;

import com.hj.excel.config.ColumnConfig;
import com.hj.excel.config.ColumnConfigUtil;
import com.hj.excel.exception.ExcelException;
import com.hj.excel.annotation.ExcelExport;
import com.hj.excel.converter.ExcelConverter;
import com.hj.excel.enums.CellType;
import com.hj.excel.enums.ExcelVersion;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @author hj
 * @Date 2022/1/4
 * @Description: 导出excel
 */
public class ExcelExportWriter<T> {

    private final Logger logger = LoggerFactory.getLogger(ExcelExportWriter.class);

    public static final float BASE_WIDTH_1_PX = 35.7f;
    public static final float BASE_HEIGHT_1_PX = 15.625f;
    public static final float BASE_CHINESE = 2 * 256;


    private final Workbook workbook;
    private final ExcelExport excelExport;
    private final Class<T> clazz;

    public ExcelExportWriter(Class<T> clazz) {
        this.clazz = clazz;
        ExcelExport excelExport = clazz.getAnnotation(ExcelExport.class);
        if (excelExport == null) {
            throw new ExcelException(
                    String.format("导出类[%s]需要注解 @ExcelExport", clazz.getName())
            );
        }
        this.excelExport = excelExport;

        ExcelVersion excelVersion = excelExport.version();
        switch (excelVersion) {
            case EXCEL2003:
                workbook = new HSSFWorkbook();
                break;
            case EXCEL2007:
                boolean fastModel = excelExport.fastModel();
                if (fastModel) {
                    int rowAccessWindowSize = excelExport.rowAccessWindowSize();
                    workbook = new SXSSFWorkbook(rowAccessWindowSize);
                } else {
                    workbook = new XSSFWorkbook();
                }
                break;
            default:
                throw new ExcelException("导出的excel版本不支持");
        }
    }

    /**
     * 创建单元格样式
     *
     * @param font       字体
     * @param background 背景色
     * @param center     是否居中
     * @param wrapText   是否自动换行
     * @return 样式
     */
    public CellStyle createCellStyle(Font font, IndexedColors background, boolean center, boolean wrapText) {
        CellStyle createCellStyle = workbook.createCellStyle();
        if (null != font) {
            createCellStyle.setFont(font);
        }
        createCellStyle.setFillForegroundColor(background.getIndex());
        createCellStyle.setFillPattern(CellStyle.SOLID_FOREGROUND);
        createCellStyle.setBorderBottom(CellStyle.BORDER_THIN);
        createCellStyle.setBorderLeft(CellStyle.BORDER_THIN);
        createCellStyle.setBorderRight(CellStyle.BORDER_THIN);
        createCellStyle.setBorderTop(CellStyle.BORDER_THIN);
        if (center) {
            // 水平居中,垂直居中
            createCellStyle.setAlignment(CellStyle.ALIGN_CENTER);
            createCellStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        }
        createCellStyle.setWrapText(wrapText);
        return createCellStyle;
    }

    /**
     * 创建字体
     *
     * @param family 字体种类
     * @param color  字体颜色
     * @param size   字体大小
     * @param bold   是否加粗
     * @return 字体
     */
    public Font createCellFont(String family, IndexedColors color, short size, boolean bold) {
        Font createFont = workbook.createFont();
        createFont.setCharSet(Font.DEFAULT_CHARSET);
        createFont.setColor(color.getIndex());
        createFont.setFontName(family);
        createFont.setFontHeightInPoints(size);
        if (bold) {
            createFont.setBoldweight((short) 700);
        }
        return createFont;
    }

    /**
     * 创建字体
     *
     * @param family    字体种类
     * @param color     字体颜色
     * @param size      字体大小
     * @param bold      是否加粗
     * @param italic    是否斜体
     * @param underline 是否加下划线
     * @return 字体
     */
    public Font createCellFont(String family, IndexedColors color, short size, boolean bold,
                               boolean italic, boolean underline) {
        Font createFont = createCellFont(family, color, size, bold);
        createFont.setItalic(italic);
        if (underline) {
            createFont.setUnderline((byte) 1);
        }
        return createFont;
    }


    /**
     * 导出 excel
     *
     * @param title      标题
     * @param data       数据列表
     * @param titleStyle 标题样式
     * @param headStyle  头部样式
     * @param bodyStyle  主体数据样式
     * @return Workbook
     */
    public Workbook export(String title, List<T> data, CellStyle titleStyle, CellStyle headStyle, CellStyle bodyStyle) {
        if (data == null || data.size() == 0) {
            //无数据直接反回空
            return null;
        }
        //解析列配置
        List<ColumnConfig<Object, Object>> columnConfigs = ColumnConfigUtil.parseColumnConfig(clazz, true);

        ExcelVersion version = excelExport.version();
        int sheetMaxRow = -1;
        if (excelExport.sheetMaxRow() == -1 && version == ExcelVersion.EXCEL2003) {
            //设置配置为最大行数
            sheetMaxRow = 60000;
        }
        try {
            //计算数据是否超量,是否需要创建多个 sheet
            List<Sheet> sheets = new ArrayList<>();
            if (sheetMaxRow == -1 || data.size() <= sheetMaxRow) {
                //只会创建一个 sheet
                Sheet createSheet = workbook.createSheet("全部数据");
                sheets.add(createSheet);
            } else {
                int sheetCount = (data.size() - 1) / sheetMaxRow + 1;
                for (int i = 0; i < sheetCount; i++) {
                    Sheet createSheet = workbook.createSheet("部分数据_part" + i);
                    sheets.add(createSheet);
                }
            }
            //正式添加数据
            if (sheets.size() == 1) {
                Sheet sheet = sheets.get(0);
                int startRow = createSheetTitle(title, titleStyle, columnConfigs, sheet);
                insertDataToSheet(sheet, data, columnConfigs, startRow, headStyle, bodyStyle);
            } else {
                for (int i = 0; i < sheets.size(); i++) {
                    Sheet sheet = sheets.get(i);
                    //如果有标题,添加标题
                    int startRow = createSheetTitle(title, titleStyle, columnConfigs, sheet);
                    //复制截断的数据,到数据表 sheet 页
                    int startDataIndex = i * sheetMaxRow;
                    int endDataIndex = (i + 1) * sheetMaxRow;
                    if (endDataIndex > data.size()) {
                        endDataIndex = data.size();
                    }
                    List<T> partData = new ArrayList<T>();
                    for (int j = startDataIndex; j < endDataIndex; j++) {
                        partData.add(data.get(j));
                    }
                    insertDataToSheet(sheet, partData, columnConfigs, startRow, headStyle, bodyStyle);
                }
            }
        } catch (Exception e) {
            throw new ExcelException("注入数据出错 insertDataToSheet " + e.getMessage(), e);
        }
        return workbook;
    }

    /**
     * 导出
     *
     * @param data      数据列表
     * @param headStyle 头部样式
     * @param bodyStyle 主体数据样式
     * @return Workbook
     */
    public Workbook export(List<T> data, CellStyle headStyle, CellStyle bodyStyle) {
        return export("", data, null, headStyle, bodyStyle);
    }

    /**
     * 导出
     *
     * @param data 数据列表
     * @return Workbook
     */
    public Workbook export(List<T> data) {
        return export(data, null, null);
    }

    /**
     * 导出
     *
     * @param title 标题
     * @param data  数据列表
     * @return Workbook
     */
    public Workbook export(String title, List<T> data) {
        return export(title, data, null);
    }

    /**
     * 导出
     *
     * @param title      标题
     * @param data       数据列表
     * @param titleStyle 标题样式
     * @return Workbook
     */
    public Workbook export(String title, List<T> data, CellStyle titleStyle) {
        return export(title, data, titleStyle, null, null);
    }

    /**
     * 当前的 Workbook转为输出流
     *
     * @throws IOException 异常
     */
    public void writeTo(OutputStream outputStream) throws IOException {
        workbook.write(outputStream);
    }

    /**
     * 当前的 Workbook转为输入流
     *
     * @return InputStream
     * @throws IOException 异常
     */
    public InputStream toInputStream() throws IOException {
        return toInputStream(workbook);
    }

    /**
     * 创建输入流
     *
     * @param workbook Workbook
     * @return InputStream
     * @throws IOException 异常
     */
    public static InputStream toInputStream(Workbook workbook) throws IOException {
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
            workbook.write(byteArrayOutputStream);
            return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        }
    }

    /**
     * 以默认样式导出
     *
     * @param title 标题
     * @param data  数据列表
     * @return Workbook
     */
    public Workbook exportDefaultStyle(String title, List<T> data) {
        CellStyle titleStyle = createCellStyle(null, IndexedColors.WHITE, true, false);
        CellStyle headStyle = createCellStyle(null, IndexedColors.LIGHT_GREEN, true, false);
        CellStyle bodyStyle = createCellStyle(null, IndexedColors.LIGHT_YELLOW, true, false);
        return export(title, data, titleStyle, headStyle, bodyStyle);
    }

    /**
     * 以默认样式导出
     *
     * @param data 数据列表
     * @return Workbook
     */
    public Workbook exportDefaultStyle(List<T> data) {
        return exportDefaultStyle("", data);
    }

    /**
     * 创建sheet标题,如果存在
     *
     * @param title         标题
     * @param titleStyle    标题样式
     * @param columnConfigs 列配置
     * @param sheet         sheet
     * @return 当前sheet的起始行
     */
    private int createSheetTitle(String title, CellStyle titleStyle, List<ColumnConfig<Object, Object>> columnConfigs, Sheet sheet) {
        int startRow = 0;
        if (StringUtils.isNotBlank(title)) {
            Row titleRow = sheet.createRow(startRow++);
            Cell titleCell = titleRow.createCell(0);
            titleCell.setCellValue(title);
            if (titleStyle != null) {
                titleCell.setCellStyle(titleStyle);
            }
            //合并单元格
            sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, columnConfigs.size() - 1));
            short titleRowHeight = excelExport.titleRowHeight();
            titleRowHeight = (short) (titleRowHeight * BASE_HEIGHT_1_PX);
            titleRow.setHeight(titleRowHeight);
        }
        return startRow;
    }

    /**
     * 添加数据到sheet
     *
     * @param sheet         sheet
     * @param partData      数据列表
     * @param columnConfigs 列配置
     * @param startRow      开始行
     * @param headStyle     头部样式
     * @param bodyStyle     主体样式
     * @throws Exception 异常
     */
    private void insertDataToSheet(Sheet sheet, List<T> partData, List<ColumnConfig<Object, Object>> columnConfigs,
                                   int startRow, CellStyle headStyle, CellStyle bodyStyle) throws Exception {
        Row headRow = sheet.createRow(startRow++);
        headRow.setHeight((short) (excelExport.headRowHeight() * BASE_HEIGHT_1_PX));
        //创建标题列
        for (int i = 0; i < columnConfigs.size(); i++) {
            ColumnConfig<Object, Object> columnConfig = columnConfigs.get(i);
            String chinese = columnConfig.getChinese();
            Cell headCell = headRow.createCell(i);
            headCell.setCellValue(chinese);
            if (null != headStyle) {
                headCell.setCellStyle(headStyle);
            }
        }

        //创建数据列
        for (T partDatum : partData) {
            Row bodyRow = sheet.createRow(startRow++);
            bodyRow.setHeight((short) (excelExport.bodyRowHeight() * BASE_HEIGHT_1_PX));
            for (int j = 0; j < columnConfigs.size(); j++) {
                ColumnConfig<Object, Object> columnConfig = columnConfigs.get(j);
                Method readMethod = columnConfig.getReadMethod();
                Cell bodyCell = bodyRow.createCell(j);
                if (bodyStyle != null) {
                    bodyCell.setCellStyle(bodyStyle);
                }
                // 设置 bodyCell 单元格格式
                CellType cellType = columnConfig.getCellType();
                bodyCell.setCellType(cellType.getValue());

                Object cellData = readMethod.invoke(partDatum);
                ExcelConverter<Object, Object> excelConverter = columnConfig.getExcelConverter();
                //如果存在转换器，则转换数据
                if (null != excelConverter) {
                    cellData = excelConverter.convert(cellData);
                }
                Class<?> dataType = columnConfig.getDataType();

                //解决数据为 null 的情况，设置对应的空值
                if (cellData == null) {
                    switch (cellType) {
                        case CELL_TYPE_STRING:
                            bodyCell.setCellType(Cell.CELL_TYPE_STRING);
                            bodyCell.setCellValue("");
                            break;
                        case CELL_TYPE_BOOLEAN:
                            bodyCell.setCellType(Cell.CELL_TYPE_BOOLEAN);
                            bodyCell.setCellValue(false);
                            break;
                        case CELL_TYPE_NUMERIC:
                            bodyCell.setCellType(Cell.CELL_TYPE_NUMERIC);
                            bodyCell.setCellValue(0);
                            break;
                        default:
                            break;
                    }
                    continue;
                }

                //如果数据类型是 double 或 Double 需要设置精度值
                if (dataType == double.class || dataType == Double.class
                        || dataType == float.class || dataType == Float.class) {
                    String cellDataString = String.valueOf(cellData);
                    int precision = columnConfig.getPrecision();
                    if (precision != -1) {
                        BigDecimal bigDecimal = new BigDecimal(cellDataString);
                        if (dataType == double.class || dataType == Double.class) {
                            cellData = bigDecimal.setScale(precision, RoundingMode.HALF_EVEN).doubleValue();
                        } else {
                            cellData = bigDecimal.setScale(precision, RoundingMode.HALF_EVEN).floatValue();
                        }
                    }
                }

                switch (cellType) {
                    case CELL_TYPE_STRING:
                        bodyCell.setCellType(Cell.CELL_TYPE_STRING);
                        if (dataType == Date.class) {
                            //获取日期对象数据
                            Date cellDataReal;
                            cellDataReal = (Date) cellData;
                            //如果是日期类型,则调用转换规则进行转换
                            String pattern = columnConfig.getPattern();
                            if (StringUtils.isBlank(pattern)) {
                                //如果是空格式,直接设置日期数据
                                bodyCell.setCellValue(cellDataReal);
                            } else {
                                bodyCell.setCellValue(DateFormatUtils.format(cellDataReal, pattern));
                            }
                        } else {
                            bodyCell.setCellValue(ObjectUtils.toString(cellData));
                        }
                        break;
                    case CELL_TYPE_NUMERIC:
                        bodyCell.setCellType(Cell.CELL_TYPE_NUMERIC);
                        String cellDataString = ObjectUtils.toString(cellData, "0");
                        boolean primitiveOrWrapper = ClassUtils.isPrimitiveOrWrapper(dataType);
                        if (primitiveOrWrapper) {
                            if (dataType == int.class || dataType == Integer.class) {
                                bodyCell.setCellValue(NumberUtils.toInt(cellDataString));
                            } else if (dataType == long.class || dataType == Long.class) {
                                bodyCell.setCellValue(NumberUtils.toLong(cellDataString));
                            } else if (dataType == double.class || dataType == Double.class) {
                                bodyCell.setCellValue(NumberUtils.toDouble(cellDataString));
                            } else if (dataType == float.class || dataType == Float.class) {
                                bodyCell.setCellValue(NumberUtils.toFloat(cellDataString));
                            } else {
                                logger.warn("单元格类型设置 numeric ,但数据类型不支持; 列为:" + columnConfig.getPropertyName() + ",类型为:" + dataType + "，支持的类型有[int,long,double,float]");
                            }
                        } else if (dataType == Date.class) {
                            //如果日期需要设置进数值单元格，强转化为毫秒值
                            Date cellDataReal = (Date) cellData;
                            bodyCell.setCellValue(cellDataReal.getTime());
                        } else {
                            logger.warn("单元格类型设置 numeric ,但数据类型不是 numeric ; 列为:" + columnConfig.getPropertyName() + ",类型为:" + dataType);
                        }
                        break;
                    case CELL_TYPE_BOOLEAN:
                        if (dataType == Boolean.TYPE || dataType == Boolean.class) {
                            assert cellData instanceof Boolean;
                            bodyCell.setCellValue((Boolean) cellData);
                        } else {
                            logger.warn("单元格类型设置 boolean ,但数据类型不是 Boolean ; 列为:" + columnConfig.getPropertyName() + "，类型为:" + dataType);
                        }
                        break;
                    case CELL_TYPE_BLANK:
                        //空值不设置数据
                        break;
                    default:
                        break;
                }

            }
        }

        //设置列宽
        boolean autoWidth = excelExport.autoWidth();
        if (autoWidth) {
            //自动列宽后使用两倍自动列宽
            for (int i = 0; i < columnConfigs.size(); i++) {
                sheet.autoSizeColumn(i);
                ColumnConfig<Object,Object> columnConfig = columnConfigs.get(i);

                int width = sheet.getColumnWidth(i);
                String titleChinese = columnConfig.getChinese();
                float titleWidth = titleChinese.length() * BASE_CHINESE;
                if (width < titleWidth) {
                    width = (int) titleWidth;
                    sheet.setColumnWidth(i, width);
                }

                if (columnConfig.isChineseWidth()) {
                    // 宽度设置为原来两倍,并且加一个中文字宽度
                    int width2 = (int) (width * 2 + 1 * BASE_CHINESE);
                    // 解决最大宽度超出限制问题
                    if (width2 > 65280) {
                        width2 = 65280;
                    }
                    sheet.setColumnWidth(i, width2);
                }
            }
        } else {
            //宽度配置策略 如果没有配置任何宽度,则取标题中文字宽度,如果有配置,则使用配置
            for (int i = 0; i < columnConfigs.size(); i++) {
                ColumnConfig<Object,Object> columnConfig = columnConfigs.get(i);
                // 增加列宽配置策略
                int width = columnConfig.getWidth();
                int charWidth = columnConfig.getCharWidth();
                int pxWidth = columnConfig.getPxWidth();
                int finalWidth;
                if (width == -1 && charWidth == -1 && pxWidth == -1) {
                    //没有配置任何宽度,使用标题中文字宽度
                    finalWidth = (int) (columnConfig.getChinese().length() * BASE_CHINESE);
                } else {
                    if (width != -1) {
                        finalWidth = width;
                    } else if (charWidth != -1) {
                        finalWidth = (int) (charWidth * BASE_CHINESE);
                    } else {
                        finalWidth = (int) (pxWidth * BASE_WIDTH_1_PX);
                    }
                }
                // 解决最大宽度超出限制问题
                if (finalWidth > 65280) {
                    finalWidth = 65280;
                }
                sheet.setColumnWidth(i, finalWidth);
                //end 增加列宽配置策略
            }
        }

        //隐藏列配置
        for (int i = 0; i < columnConfigs.size(); i++) {
            ColumnConfig<Object,Object> columnConfig = columnConfigs.get(i);
            boolean hidden = columnConfig.isHidden();
            sheet.setColumnHidden(i, hidden);
        }
    }

}
